iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
JavaScript

不要再說你不會後端!30 天 Node.js 降維打擊指南 系列 第 5 篇

Day 5:Stream 與 Buffer 的記憶體防禦戰

  • 分享至 

  • xImage
  •  

在昨天的 HTTP 伺服器實作中,我們學會了如何回傳簡單的文字與 JSON。但實務上,後端常常需要處理「巨大的檔案」,例如 2GB 的影片或幾十萬筆的 CSV 報表。

如果你用 Day 3 學到的 fs.readFile 去讀取一個 2GB 的檔案,Node.js 會試圖把這 2GB 的資料全部塞進伺服器的記憶體 (RAM) 裡。如果同時有十個使用者請求這個檔案,你的伺服器記憶體會瞬間爆滿,直接當機(Out of Memory)。

為了解決這個問題,Node.js 提供了處理大數據的秘密武器:Buffer (緩衝區) 與 Stream (資料流)。


1. 核心概念:水桶與水管

要搞懂這兩個名詞,請想像你要把一個巨大的水塔(大檔案)裡的水,轉移到另一個空水塔(使用者端)。

  • 傳統作法 (fs.readFile):準備一個和水塔一樣大的超級大水桶,把水全部裝進去,再一次倒過去。這個超級大水桶,就是伺服器的記憶體。
  • Buffer (緩衝區):這是一個「標準尺寸的小水桶」。Node.js 在處理底層二進位 (Binary) 資料時,會把資料切碎放在這些小水桶裡。
  • Stream (資料流):這是一根「水管」。我們不把水裝滿,而是用水管連接兩個水塔,水一邊流進來(讀取),就一邊流出去(寫入或回傳)。不管水塔有幾萬噸(檔案有多大),我們消耗的資源永遠只有那根水管的粗細。

2. 實作練習:用 Stream 打造不崩潰的伺服器

我們將結合昨天的 HTTP 模組,寫一個能夠「串流」傳輸檔案的伺服器。就算檔案有幾 GB,這支程式的記憶體佔用也能穩定維持在幾十 MB 以內。

步驟 1:準備一個(假的)大檔案

在 hello-node 資料夾下,新增一個 stream-demo.js。我們順便教你怎麼用傳統方式寫入大量資料來製造一個大檔案(請注意這一步會稍等幾秒鐘):

import fs from 'fs';

console.log(" 正在製造假的大檔案,請稍候...");
const file = fs.createWriteStream('./big-data.txt');

// 寫入 10 萬行文字,製造出一個稍微大一點的檔案
for (let i = 0; i < 100000; i++) {
    file.write(`這是第 ${i} 行:Node.js Stream 降維打擊測試資料!\n`);
}
file.end();
console.log(" 大檔案製造完成!");

在終端機執行 node stream-demo.js,你的資料夾會多出一個 big-data.txt。
image

步驟 2:實作 Stream HTTP 伺服器

現在,清空剛才的 stream-demo.js(或直接修改它),換成以下結合 HTTP 與 Stream 的程式碼:

import http from 'http';
import fs from 'fs';

const server = http.createServer((req, res) => {
    if (req.url === '/') {
        // 設定標頭,告訴瀏覽器這是一份純文字檔案
        res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });

        // 1. 建立「讀取流 (水管的進水口)」
        const readStream = fs.createReadStream('./big-data.txt', 'utf-8');

        // 2. 將讀取流透過 pipe (管線) 直接對接到 res (回應流 / 水管的出水口)
        readStream.pipe(res);

        // 監聽進階事件 (可選):當資料流結束時,印出提示
        readStream.on('end', () => {
            console.log(" 串流傳輸完畢!");
        });

        // 錯誤處理:檔案不存在時的防禦
        readStream.on('error', (err) => {
            res.writeHead(500);
            res.end("伺服器發生錯誤");
            console.error("讀取失敗:", err.message);
        });
    } else {
        res.writeHead(404);
        res.end("Not Found");
    }
});

const PORT = 3000;
server.listen(PORT, () => {
    console.log(` Stream 伺服器啟動!請前往:http://localhost:${PORT}`);
});

步驟 3:見證魔法

  1. 在終端機執行 node stream-demo.js。
  2. 打開瀏覽器前往 http://localhost:3000。
  3. 你會看到 10 萬行文字被順暢地印在畫面上。
    image
    我截個大概

雖然這個範例的檔案不到 10MB,但即使你把剛才的回圈加到 1000 萬行(變成幾 GB 的檔案),這段使用了 readStream.pipe(res) 的程式碼也絕對不會因為記憶體不足而崩潰。


上一篇
Day 4:內建核心模組實戰(fs, path, http)
下一篇
Day 6:事件驅動架構與 EventEmitter 的廣播魔法
系列文
不要再說你不會後端!30 天 Node.js 降維打擊指南 共 16 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
lin1015
iT邦新手 5 級 ‧ 2026-09-19 18:31:09

水桶與水管的比喻把 Buffer、Stream 和記憶體差異講得很直觀,也順帶帶到 pipe 的背壓處理。範例先送出 200 後,如果串流途中才 error,這時 500 標頭可能已來不及改;你下一篇會示範 pipeline() 或 headersSent 的錯誤處理嗎?

我要留言

立即登入留言